##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary

  # Exploit mixins should be called first
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::Remote::SMB::Client::Authenticated

  include Msf::Exploit::Remote::DCERPC

  # Scanner mixin should be near last
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  include Msf::OptionalSession::SMB

  def initialize
    super(
      'Name'        => 'SMB User Enumeration (SAM EnumUsers)',
      'Description' => 'Determine what local users exist via the SAM RPC service',
      'Author'      => 'hdm',
      'License'     => MSF_LICENSE,
      'DefaultOptions' => {
        'DCERPC::fake_bind_multi' => false
      },
    )

    register_options(
      [
        OptBool.new('DB_ALL_USERS', [ false, "Add all enumerated usernames to the database", false ]),
      ])

    deregister_options('RPORT')
  end

  def rport
    @rport || super
  end

  def smb_direct
    @smbdirect || super
  end

  # Locate an available SMB PIPE for the specified service
  def smb_find_dcerpc_pipe(uuid, vers, pipes)
    found_pipe   = nil
    found_handle = nil
    pipes.each do |pipe_name|
      connected = session ? true : false
      begin
        unless connected
          connect
          smb_login
          connected = true
        end

        handle = dcerpc_handle_target(
          uuid, vers,
          'ncacn_np', ["\\#{pipe_name}"], simple.address
        )

        dcerpc_bind(handle)
        return pipe_name

      rescue ::Interrupt => e
        raise e
      rescue ::Exception => e
        raise e if not connected
      end
      disconnect
    end
    nil
  end

  def smb_pack_sid(str)
    [1,5,0].pack('CCv') + str.split('-').map{|x| x.to_i}.pack('NVVVV')
  end

  def smb_parse_sam_domains(data)
    ret = []
    idx = 0

    cnt = data[8, 4].unpack("V")[0]
    return ret if cnt == 0
    idx += 20
    idx += 12 * cnt

    1.upto(cnt) do
      v = data[idx,data.length].unpack('V*')
      l = v[2] * 2

      while(l % 4 != 0)
        l += 1
      end

      idx += 12
      ret << data[idx, v[2] * 2].gsub("\x00", '')
      idx += l
    end
    ret
  end

  def smb_parse_sam_users(data)
    ret = {}
    rid = []
    idx = 0

    cnt = data[8, 4].unpack("V")[0]
    return ret if cnt == 0
    idx += 20

    1.upto(cnt) do
      v = data[idx,12].unpack('V3')
      rid << v[0]
      idx += 12
    end

    1.upto(cnt) do
      v = data[idx,32].unpack('V*')
      l = v[2] * 2

      while(l % 4 != 0)
        l += 1
      end

      uid = rid.shift

      idx += 12
      ret[uid] = data[idx, v[2] * 2].gsub("\x00", '')
      idx += l
    end

    ret
  end

  @@sam_uuid     = '12345778-1234-abcd-ef00-0123456789ac'
  @@sam_vers     = '1.0'
  @@sam_pipes    = %W{ SAMR LSARPC NETLOGON BROWSER SRVSVC }

  # Fingerprint a single host
  def run_host(ip)
    ports = [139, 445]

    if session
      print_status("Using existing session #{session.sid}")
      client = session.client
      self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client)
      ports = [simple.port]
      self.simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too
    end

    ports.each do |port|

      @rport = port

      sam_pipe   = nil
      sam_handle = nil
      begin
        # Find the SAM pipe
        sam_pipe = smb_find_dcerpc_pipe(@@sam_uuid, @@sam_vers, @@sam_pipes)
        break if not sam_pipe

        # Connect4
        stub =
          NDR.uwstring("\\\\" + simple.address) +
          NDR.long(2) +
          NDR.long(0x30)

        dcerpc.call(62, stub)
        resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil

        if ! (resp and resp.length == 24)
          print_error("Invalid response from the Connect5 request")
          disconnect
          return
        end

        phandle = resp[0,20]
        perror  = resp[20,4].unpack("V")[0]

        if(perror == 0xc0000022)
          disconnect
          return
        end

        if(perror != 0)
          print_error("Received error #{"0x%.8x" % perror} from the OpenPolicy2 request")
          disconnect
          return
        end

        # EnumDomains
        stub = phandle + NDR.long(0) + NDR.long(8192)
        dcerpc.call(6, stub)
        resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
        domlist = smb_parse_sam_domains(resp)
        domains = {}

        # LookupDomain
        domlist.each do |domain|
          next if domain == 'Builtin'

          # Round up the name to match NDR.uwstring() behavior
          dlen = (domain.length + 1) * 2

          # The SAM functions are picky on Windows 2000
          stub =
            phandle +
            [(domain.length + 0) * 2].pack("v") + # NameSize
            [(domain.length + 1) * 2].pack("v") + # NameLen (includes null)
            NDR.long(rand(0x100000000)) +
            [domain.length + 1].pack("V") +	      # MaxCount (includes null)
            NDR.long(0) +
            [domain.length + 0].pack("V") +	      # ActualCount (ignores null)
            Rex::Text.to_unicode(domain)          # No null appended

          dcerpc.call(5, stub)
          resp    = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
          raw_sid = resp[12, 20]
          txt_sid = raw_sid.unpack("NVVVV").join("-")

          domains[domain] = {
            :sid_raw => raw_sid,
            :sid_txt => txt_sid
          }
        end


        # OpenDomain, QueryDomainInfo, CloseDomain
        domains.each_key do |domain|

          # Open
          stub =
            phandle +
            NDR.long(0x00000305) +
            NDR.long(4) +
            [1,4,0].pack('CvC') +
            domains[domain][:sid_raw]

          dcerpc.call(7, stub)
          resp    = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
          dhandle = resp[0,20]
          derror  = resp[20,4].unpack("V")[0]

          # Catch access denied replies to OpenDomain
          if(derror != 0)
            next
          end

          # Password information
          stub = dhandle + [0x01].pack('v')
          dcerpc.call(8, stub)
          resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
          if(resp and resp[-4,4].unpack('V')[0] == 0)
            mlen,hlen = resp[8,4].unpack('vv')
            domains[domain][:pass_min] = mlen
            domains[domain][:pass_min_history] = hlen
          end

          # Server Role
          stub = dhandle + [0x07].pack('v')
          dcerpc.call(8, stub)
          if(resp and resp[-4,4].unpack('V')[0] == 0)
            resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
            domains[domain][:server_role] = resp[8,2].unpack('v')[0]
          end

          # Lockout Threshold
          stub = dhandle + [12].pack('v')
          dcerpc.call(8, stub)
          resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil

          if(resp and resp[-4,4].unpack('V')[0] == 0)
            lduration = resp[8,8]
            lwindow   = resp[16,8]
            lthresh   = resp[24, 2].unpack('v')[0]

            domains[domain][:lockout_threshold] = lthresh
            domains[domain][:lockout_duration]  = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lduration.unpack('V2')))
            domains[domain][:lockout_window]    = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lwindow.unpack('V2')))
          end

          # Users
          stub = dhandle + NDR.long(0) + NDR.long(0x10) + NDR.long(1024*1024)
          dcerpc.call(13, stub)
          resp  = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
          if(resp and resp[-4,4].unpack('V')[0] == 0)
            domains[domain][:users] = smb_parse_sam_users(resp)
          end


          # Close Domain
          dcerpc.call(1, dhandle)
        end

        # Close Policy
        dcerpc.call(1, phandle)


        domains.each_key do |domain|

          # Delete the no longer used raw SID value
          domains[domain].delete(:sid_raw)

          # Store the domain name itself
          domains[domain][:name] = domain

          # Store the domain information
          report_note(
            :host => simple.address,
            :proto => 'tcp',
            :port => rport,
            :type => 'smb.domain.enumusers',
            :data => domains[domain]
          )

          users = domains[domain][:users] || {}
          extra = ""
          if (domains[domain][:lockout_threshold])
            extra = "( "
            extra << "LockoutTries=#{domains[domain][:lockout_threshold]} "
            extra << "PasswordMin=#{domains[domain][:pass_min]} "
            extra << ")"
          end
          print_good("#{domain.upcase} [ #{users.keys.map{|k| users[k]}.join(", ")} ] #{extra}")
          if datastore['DB_ALL_USERS']
            users.each { |user|
              store_username(user, domain, simple.address, rport, resp)
            }
          end
        end

        # cleanup
        disconnect
        return
      rescue ::Timeout::Error
      rescue ::Interrupt
        raise $!
      rescue ::Rex::ConnectionError
      rescue ::Rex::Proto::SMB::Exceptions::LoginError
        next
      rescue ::Exception => e
        print_line("Error: #{simple.address} #{e.class} #{e}")
      end
    end
  end


  def store_username(username, domain, ip, rport, resp)
    service_data = {
      address: ip,
      port: rport,
      service_name: 'smb',
      protocol: 'tcp',
      workspace_id: myworkspace_id,
      proof: resp
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: username[1],
      realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
      realm_value: domain,
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end

end
